[모바일 해킹 연습] Uncrackable3 풀이

[모바일 해킹 연습] Uncrackable3 풀이

Lecture
Security
태그
uncrackable
frida
mobile hacking
public
완성
Y
생성일
Mar 18, 2024 05:48 AM
LectureName
Mobile Hacking (Android - Frida )

1. Uncrackable3


1.1 다운로드

해당 웹사이트에서 다운 받을 수 있으며, view Raw 버튼을 누르면 다운로드 됩니다.
 
 

1.2 실습 준비 도구

유형 별로 정리해 두었습니다. 유형 별로 맘에 드는 도구를 선택하여 사용하시면 됩니다.
 
Apk 분석 툴
  • jadx (추천) → 현재 풀이에서 사용
  • BytecodeViewer
  • jd-gui & dex2jar
 
리버스 엔지니어링 도구
  • IDA → 현재 풀이에서 사용
  • Gidra
 
Frida
  • Frida (client, server) 환경 구축 완료 상태
 
device
  • mobile device ( 루팅폰 ) → 현재 풀이에서 사용
  • ADV ( 루팅 )
  • NOX ( 루팅 )
 
 
 
 

2. Uncrackable3 풀이


2.1 루팅 탐지 우회 확인

Uncrackable2 와 마찬가지로, 애플리케이션을 실행하면 기기가 루팅이 되었는지 확인하고, Root or tampering detected 라는 문자열을 출력하며 종료됩니다.
 
이는 exit 함수를 후킹하거나, 루팅 탐지 함수를 우회하는 것으로 해결했었습니다.
이번에도 기존 방식과 동일하게 탐지를 우회하여 보겠습니다.
 
루팅 탐지 로직 확인
➡️ MainActivity onCreate() 부분을 보면, RootDetection class의 method로 루팅을 탐지합니다.
notion image
 
 
➡️ 해당 클래스(RootDetection)를 보면, 3개의 method가 루팅탐지에 이용됩니다.
notion image
  • su 확인
  • 빌드 태그를 확인
  • 루팅에 사용되는 주요 프로그램이 설치된 경로 확인
 
 
➡️ Uncrackable2와 같이 해당 method를 전부 false 를 return 하도록 재 작성 해보겠습니다.
setImmediate(function() { Java.perform(function() { let rootDetectionModule = Java.use('sg.vantagepoint.util.RootDetection'); rootDetectionModule.checkRoot1.implementation = function() { console.log('[+] check root 1 bypass'); return false; }; rootDetectionModule.checkRoot2.implementation = function() { console.log('[+] check root 2 bypass'); return false; }; rootDetectionModule.checkRoot3.implementation = function() { console.log('[+] check root 3 bypass'); return false; }; }); }); //frida -U -f owasp.mstg.uncrackable3 -l <script_name>
 
 
 

2.2 크래시 확인

➡️ 해당 스크립트를 frida를 이용하여 동작하였지만, 애플리케이션이 중단되는 결과가 발생합니다.
notion image
 
➡️ 원인 분석을 위해 logcat으로 로그를 확인해 보겠습니다.
notion image
  • 루팅 탐지 로직에서 발견하지 못한 문자열들이 존재합니다.
  • 루팅 탐지에 추가 기능이 있거나, 무언가 방어 대책이 존재하는 것 같습니다.
 
 
크래시 원인 분석
➡️ 크래시가 나는 이유를 확인하기 위해, 루팅탐지 이전에 동작한 verifyLibs() 를 확인해 보겠습니다.
notion image
➡️ verifyLibs() 를 확인해보면, 뭔가 라이브러리에 대한 무결성을 확인하는 것 같습니다.
notion image
  • 아까 로그에 보았던 CRC[ ~ 형태의 문자열도 보입니다.
  • 하지만 앞서 logcat으로 보았던 로그에 찍힌 Tampering detected! 문자열은 보이지 않습니다.
 
 
➡️ 해당 문자열이 class 파일 안에 있는지 확인하기 위해 검색하였지만, 존재하지 않습니다.
notion image
  • 따라서 해당 탐지 로직은, NativeCode로 작동될 확률이 높다고 판단, 라이브러리를 로드하는 곳을 확인해야 합니다. (아니면 backtrace를 확인하여도 됩니다.)
 
 
➡️ JNI를 사용한다고 판단하고, 해당 라이브러리가 Load 되는 곳을 확인하였습니다.
notion image
  • 해당 라이브러리는 어플리케이션의 /lib/<arch_name>/libfoo 로 저장되게 됩니다.
 
 
 

2.3 Native 코드 분석

➡️ 해당 라이브러리를 IDA 로 분석하여 Tampering detected! 문자열을 찾았습니다.
notion image
  • IDA에서는 F12 + Shift 를 누르면, 문자열들을 볼 수 있습니다.
 
 
➡️ 해당 문자열이 참조된 함수를 확인할 수 있습니다.
notion image
  • F5 키를 눌러 디컴파일을 진행합니다.
 
 
➡️ start_routine 이라는 함수에서 frida 를 탐지하고 있었습니다.
notion image
  • start_routine 함수를 트레이싱 하다보면, 해당 libc가 로드될때 실행됨을 알 수 있습니다.
  • 프로세스 중에서 frida와 관련된 문자열이 있는지 확인하고, 있다면 프로세스를 종료시킵니다.
  • strstr 함수는 서브스트링을 찾는 함수로, args[0] 에서 args[1] 문자열을 찾지 못한다면 0(Null) 을 반환합니다.
 
 
➡️ 따라서 해당 strstr이 반드시 0을 반환하도록 Intercepter 을 이용하여 후킹해 보겠습니다.
setImmediate(function() { Java.perform(function() { //strstr 함수 우회 Interceptor.attach(Module.getExportByName(null, 'strstr'), { onEnter(args) { // 프로세스 확인 let arg1 = Memory.readUtf8String(args[0]); // 프로세스에 frida가 있다면 this.frida를 activate if (arg1.includes('frida') || arg1.includes('xposed')) { this.frida = true; } }, onLeave(retval) { // this.frida가 activate 상태라면 return value를 0으로 변환 if (this.frida == true) { retval.replace(0); } } }); // 루팅 탐지 로직 우회 let rootDetectionModule = Java.use('sg.vantagepoint.util.RootDetection'); rootDetectionModule.checkRoot1.implementation = function() { console.log('[+] check root 1 bypass'); return false; }; rootDetectionModule.checkRoot2.implementation = function() { console.log('[+] check root 2 bypass'); return false; }; rootDetectionModule.checkRoot3.implementation = function() { console.log('[+] check root 3 bypass'); return false; }; }); });
  • 이후 해당 스크립트를 -f 옵션과 함께 실행하면 루팅 탐지가 우회 되는 것을 알 수 있습니다.
 
 
 

3. Secret value 찾기


3.1 정답 체크 부분 확인

➡️ Code_check class의 check_code method 부분에 EditText 에서 가져온 데이터로 SecretValue를 확인합니다.
notion image
 
 
➡️ check_code 부분을 확인하면, bar이라는 Native method로 무언가 하는 것을 알 수 있습니다.
notion image
 
 
 

3.2 NativeCode분석 (libfoo.so)

➡️ CodeCheck_bar의 코드를 살펴보면 무언가 키를 확인하는 부분이 있습니다.
notion image
 
➡️ 해당 코드를 분석해 보겠습니다.
  1. 입력값v4에 저장합니다.
  1. 입력값v7 ^ dest(xor) 에 대한 값을 1byte씩 비교하여 일치하는지 확인합니다.
  • v7은 길이 40의 배열이며, sub_12C0 함수에 인자로 들어갑니다.
  • dest또한 확인이 필요합니다.
  1. 총 0x18 (24) 만큼 byte를 비교합니다.
 
 
➡️ dest를 추적해 보겠습니다.
notion image
  • dest를 보면, NativeCode로 작성된 Init에서 참조 되어있는 것을 볼 수 있습니다.
 
 
➡️ init을 확인해 보겠습니다.
notion image
  • 인자로 값을 받아 dest의 주소에 문자열을 strncpy로 복사 하는 것을 알 수 있습니다.
 
 
➡️ 해당 Init이 호출되는 ClassCode 를 확인해 보겠습니다.
jadx로 분석한 class 파일입니다. Native가 아닙니다.
jadx로 분석한 class 파일입니다. Native가 아닙니다.
  • init 함수의 호출에 상단에 저장되었던 xorkey가 들어갑니다.
  • 따라서 xorkeypizzapizzapizzapizzapizzdest에 복사 됨을 알 수 있습니다.
 
 
➡️ 이제 dest의 값을 알았으니 v7의 값을 확인하면, secretValue를 알 수 있습니다. v7 을 분석해 보겠습니다.
notion image
  • v7은 길이 40의 char arr로 선언됩니다.
  • sub_12C0()에 버퍼의 시작 주소가 전달됩니다.
 
 
➡️ 매우 긴 함수지만, 매개변수를 참조하는 코드를 확인해보면, 마지막 줄에만 해당되는 것을 알 수 있습니다.
notion image
  • xmmword는 16Byte를 참조합니다. 따라서 v7 배열에 길이 16의 byte를 쓰는 것을 알 수 있습니다.
  • 이후 나머지 8byte는 0x14130817005A0E08 을 넣는 것을 볼 수 있습니다.
 
 
➡️ 16byte를 넣는 곳을 확인해보면, 15131D5A1903000D1549170F1311081D 를 넣는 것을 알 수 있습니다.
notion image
 
 

3.3 SecretValue 추출

v7에 들어간 16byte8byte를 합쳐서 xorkeyxor해주면, SecretValue를 확인할 수 있습니다.
key1 = bytes.fromhex("1D0811130F1749150D0003195A1D1315") little_endian_value = bytes.fromhex("14130817005A0E08") key2 = little_endian_value[::-1] key = key1 + key2 secret = key.decode("utf-8") xorkey = "pizzapizzapizzapizzapizz" password = "" for i in range(24): password += chr((ord(secret[i])^ord(xorkey[i]))) print("[!] Found flag: " + password) # [!] Found flag: making owasp great again #
  • 나머지 8byte는 리틀 엔디안 형식으로 데이터가 삽입되었기 때문에 byte단위로 reverse 해주어야 합니다.
  • key는 making owasp great again 임을 알 수 있습니다.
 
 
 

3.4 다른 풀이

💡
일단 기존 풀이는 디바이스의 아키텍쳐가 intell이였기 때문에 함수의 offset이 12C0이였습니다. 하지만 저는 arm 아키텍쳐로 풀이를 진행하기 때문에, offset이 다릅니다. 이점을 유의해야 합니다.
notion image
  • 기존 함수 (12C0)
 
➡️ sub_10E0은 libfoo가 load된 시점에서 함수의 주소가 10E0만큼 offset이 발생한다는 의미입니다.
  • 여기서 주의할 점은, ARM 아키텍쳐여서 함수의 오프셋이 10E0으로 변동되었습니다.
notion image
  • 따라서 libfoo의 메모리 로드 시점에서 메모리 주소를 구하고, 10E0만큼 더해주면 해당 함수의 주소를 알 수 있고, 인자로 전달되는 v8이 어떤식으로 값이 바뀌는지 볼 수 있습니다.
 
 
➡️ 후킹 코드는 다음과 같습니다.
// libfoo load 전까지 1초 기다림 setTimeout(function(){ // intercept sub_10E0 (arm) Interceptor.attach(Module.findBaseAddress('libfoo.so').add(0x10E0), { onEnter: function(args) { this.v8 = args[0] //v8 console.log("sub_10E0 found") }, // after sub_10E0() onLeave: function(retval) { console.log("v8 value"); console.log(hexdump(this.v8, { offset: 0, length: 0x20, header: true, ansi: true })); } }); }, 1000) setImmediate(function() { Java.perform(function() { Interceptor.attach(Module.getExportByName(null, 'strstr'), { ... 루팅탐지 우회 코드 });
 
 
➡️ 0으로 초기화 되었던 v8이 sub_10E0 이후 값이 변경된 것을 볼 수 있습니다.
notion image
  • 1D 08 11 13 0F 17 49 15 0D 00 03 19 5A 1D 13 15 08 0e 5a 00 17 08 13 14 로 변경되었습니다.
  • 앞서 기존 풀이에서 보았던 24바이트와 일치합니다.
 
 
➡️ 기존 secret 키를 구해보겠습니다.
key1 = bytes.fromhex("1D0811130F1749150D0003195A1D1315080e5a0017081314") secret = key1.decode("utf-8") xorkey = "pizzapizzapizzapizzapizz" password = "" for i in range(24): password += chr((ord(secret[i])^ord(xorkey[i]))) print("[!] Found flag: " + password) """ ─$ python3 ./test.py [!] Found flag: making owasp great again """
 
짜잔~
 
이렇게 Uncrackable level3의 풀이가 끝났습니다.
글이 길어지다보니, 이상한 설명 혹은 부족한 풀이가 있을 수 있습니다. 피드백은 언제나 환영입니다.